package de.mmis.utilities.consoleWrapper;

import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.ItemEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Stack;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.ButtonModel;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JPanel;
import javax.swing.JToggleButton.ToggleButtonModel;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.plaf.ActionMapUIResource;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

class ThreadGroupSelection extends JTree {

	private final CheckTreeSelectionModel selectionModel;
	private final ThreadTreeModel model;

	public ThreadGroupSelection(Collection<Thread> threads,
			Collection<Thread> selectedThreads) {
		model = new ThreadTreeModel(threads);
		setModel(model);

		// add checkboxes and all this stuff
		selectionModel = new CheckTreeManager(this).getSelectionModel();

		if (selectedThreads == null)
			return;

		for (Thread t : selectedThreads) {
			TreePath path = model.getPathForThread(t);
			if (path != null)
				selectionModel.addSelectionPath(path);
		}
	}

	@Override
	public void addTreeSelectionListener(TreeSelectionListener tsl) {
		selectionModel.addTreeSelectionListener(tsl);
	}

	@Override
	public void removeTreeSelectionListener(TreeSelectionListener tsl) {
		selectionModel.removeTreeSelectionListener(tsl);
	}

	public HashSet<Thread> getSelectedThreads() {
		HashSet<Thread> result = new HashSet<Thread>();

		TreePath[] paths = selectionModel.getSelectionPaths();
		if (paths == null)
			return result;

		for (TreePath path : paths) {
			Object last = path.getLastPathComponent();
			if (!(last instanceof ThreadNode))
				continue;

			ThreadNode node = (ThreadNode) last;

			// add all sub-threads
			LinkedList<ThreadNode> nodes = new LinkedList<ThreadNode>();
			nodes.add(node);
			while (!nodes.isEmpty()) {
				ThreadNode n = nodes.removeFirst();
				if (n.thread != null)
					result.add(n.thread);
				else
					nodes.addAll(n.childs);
			}

		}

		return result;
	}

	@Override
	public String convertValueToText(Object value, boolean selected,
			boolean expanded, boolean leaf, int row, boolean hasFocus) {
		if (!(value instanceof ThreadNode))
			return super.convertValueToText(value, selected, expanded, leaf,
					row, hasFocus);

		ThreadNode node = (ThreadNode) value;
		if (node.thread == null && node.group == null)
			return "All";

		if (node.thread != null)
			return node.thread.getName();

		return node.group.getName();
	}

}

class ThreadNode {
	public Thread thread;
	public ThreadGroup group;

	public LinkedList<ThreadNode> childs = new LinkedList<ThreadNode>();
	public ThreadNode parent;

	public ThreadNode(Thread thread) {
		this.thread = thread;
	}

	public ThreadNode(ThreadGroup group) {
		this.group = group;
	}
}

class ThreadTreeModel implements TreeModel {

	private HashMap<ThreadGroup, ThreadNode> threadMap = new HashMap<ThreadGroup, ThreadNode>();

	private EventListenerList listenerList = new EventListenerList();

	public ThreadTreeModel(Collection<Thread> threads) {
		for (Thread t : threads) {
			ThreadGroup group = t.getThreadGroup();
			buildTree(group, threadMap);

			ThreadNode node = new ThreadNode(t);
			ThreadNode groupNode = threadMap.get(group);
			groupNode.childs.add(node);
			node.parent = groupNode;
		}
	}

	private void buildTree(ThreadGroup group,
			HashMap<ThreadGroup, ThreadNode> map) {
		ThreadNode node;
		if (map.containsKey(group))
			return;

		map.put(group, node = new ThreadNode(group));

		if (group == null)
			return;

		ThreadGroup parent = node.group.getParent();

		buildTree(parent, map);

		ThreadNode parentNode = map.get(parent);
		node.parent = parentNode;
		parentNode.childs.add(node);
	}

	TreePath getPathForThread(Thread thread) {
		LinkedList<ThreadNode> path = new LinkedList<ThreadNode>();

		ThreadGroup group = thread.getThreadGroup();
		ThreadNode groupNode = threadMap.get(group);
		for (ThreadNode child : groupNode.childs)
			if (thread.equals(child.thread)) {
				path.add(child);
				break;
			}

		if (path.isEmpty())
			return null;

		for (; groupNode != null; groupNode = groupNode.parent)
			path.addFirst(groupNode);

		ThreadNode[] result = new ThreadNode[path.size()];
		int i = 0;
		for (ThreadNode n : path)
			result[i++] = n;

		return new TreePath(result);
	}

	@Override
	public Object getRoot() {
		ThreadNode result = threadMap.get(null);
		return result;
	}

	@Override
	public Object getChild(Object parent, int index) {
		ThreadNode parentNode = (ThreadNode) parent;
		ThreadNode result = parentNode.childs.get(index);
		return result;
	}

	@Override
	public int getChildCount(Object parent) {
		ThreadNode parentNode = (ThreadNode) parent;
		return parentNode.childs.size();
	}

	@Override
	public boolean isLeaf(Object node) {
		if (!(node instanceof ThreadNode))
			return true;

		return ((ThreadNode) node).thread != null;
	}

	@Override
	public void valueForPathChanged(TreePath path, Object newValue) {
		// empty
	}

	@Override
	public int getIndexOfChild(Object parent, Object child) {
		ThreadNode parentNode = (ThreadNode) parent;
		return parentNode.childs.indexOf(child);
	}

	@Override
	public void addTreeModelListener(TreeModelListener l) {
		listenerList.add(TreeModelListener.class, l);
	}

	@Override
	public void removeTreeModelListener(TreeModelListener l) {
		listenerList.remove(TreeModelListener.class, l);
	}

}

/*
 * the following classes are from
 * 
 * Santhosh Kumar T - santhosh@in.fiorano.com
 */
class CheckTreeSelectionModel extends DefaultTreeSelectionModel {
	private TreeModel model;

	public CheckTreeSelectionModel(TreeModel model) {
		this.model = model;
		setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
	}

	// tests whether there is any unselected node in the subtree of given path
	public boolean isPartiallySelected(TreePath path) {
		if (isPathSelected(path, true))
			return false;
		TreePath[] selectionPaths = getSelectionPaths();
		if (selectionPaths == null)
			return false;
		for (int j = 0; j < selectionPaths.length; j++) {
			if (isDescendant(selectionPaths[j], path))
				return true;
		}
		return false;
	}

	// tells whether given path is selected.
	// if dig is true, then a path is assumed to be selected, if
	// one of its ancestor is selected.
	public boolean isPathSelected(TreePath path, boolean dig) {
		if (!dig)
			return super.isPathSelected(path);
		while (path != null && !super.isPathSelected(path))
			path = path.getParentPath();
		return path != null;
	}

	// is path1 descendant of path2
	private boolean isDescendant(TreePath path1, TreePath path2) {
		Object obj1[] = path1.getPath();
		Object obj2[] = path2.getPath();
		for (int i = 0; i < obj2.length; i++) {
			if (obj1[i] != obj2[i])
				return false;
		}
		return true;
	}

	@Override
	public void setSelectionPaths(TreePath[] pPaths) {
		throw new UnsupportedOperationException("not implemented yet!!!");
	}

	@Override
	public void addSelectionPaths(TreePath[] paths) {
		// unselect all descendants of paths[]
		for (int i = 0; i < paths.length; i++) {
			TreePath path = paths[i];
			TreePath[] selectionPaths = getSelectionPaths();
			if (selectionPaths == null)
				break;
			ArrayList<TreePath> toBeRemoved = new ArrayList<TreePath>();
			for (int j = 0; j < selectionPaths.length; j++) {
				if (isDescendant(selectionPaths[j], path))
					toBeRemoved.add(selectionPaths[j]);
			}
			super.removeSelectionPaths(toBeRemoved.toArray(new TreePath[0]));
		}

		// if all siblings are selected then unselect them and select parent
		// recursively
		// otherwize just select that path.
		for (int i = 0; i < paths.length; i++) {
			TreePath path = paths[i];
			TreePath temp = null;
			while (areSiblingsSelected(path)) {
				temp = path;
				if (path.getParentPath() == null)
					break;
				path = path.getParentPath();
			}
			if (temp != null) {
				if (temp.getParentPath() != null)
					addSelectionPath(temp.getParentPath());
				else {
					if (!isSelectionEmpty())
						removeSelectionPaths(getSelectionPaths());
					super.addSelectionPaths(new TreePath[] { temp });
				}
			} else
				super.addSelectionPaths(new TreePath[] { path });
		}
	}

	// tells whether all siblings of given path are selected.
	private boolean areSiblingsSelected(TreePath path) {
		TreePath parent = path.getParentPath();
		if (parent == null)
			return true;
		Object node = path.getLastPathComponent();
		Object parentNode = parent.getLastPathComponent();

		int childCount = model.getChildCount(parentNode);
		for (int i = 0; i < childCount; i++) {
			Object childNode = model.getChild(parentNode, i);
			if (childNode == node)
				continue;
			if (!isPathSelected(parent.pathByAddingChild(childNode)))
				return false;
		}
		return true;
	}

	@Override
	public void removeSelectionPaths(TreePath[] paths) {
		for (int i = 0; i < paths.length; i++) {
			TreePath path = paths[i];
			if (path.getPathCount() == 1)
				super.removeSelectionPaths(new TreePath[] { path });
			else
				toggleRemoveSelection(path);
		}
	}

	// if any ancestor node of given path is selected then unselect it
	// and selection all its descendants except given path and descendants.
	// otherwise just unselect the given path
	private void toggleRemoveSelection(TreePath path) {
		Stack<TreePath> stack = new Stack<TreePath>();
		TreePath parent = path.getParentPath();
		while (parent != null && !isPathSelected(parent)) {
			stack.push(parent);
			parent = parent.getParentPath();
		}
		if (parent != null)
			stack.push(parent);
		else {
			super.removeSelectionPaths(new TreePath[] { path });
			return;
		}

		while (!stack.isEmpty()) {
			TreePath temp = stack.pop();
			TreePath peekPath = stack.isEmpty() ? path : stack.peek();
			Object node = temp.getLastPathComponent();
			Object peekNode = peekPath.getLastPathComponent();
			int childCount = model.getChildCount(node);
			for (int i = 0; i < childCount; i++) {
				Object childNode = model.getChild(node, i);
				if (childNode != peekNode)
					super.addSelectionPaths(new TreePath[] { temp
							.pathByAddingChild(childNode) });
			}
		}
		super.removeSelectionPaths(new TreePath[] { parent });
	}
}

class CheckTreeCellRenderer extends JPanel implements TreeCellRenderer {
	private CheckTreeSelectionModel selectionModel;
	private TreeCellRenderer delegate;
	private TristateCheckBox checkBox = new TristateCheckBox("");

	public CheckTreeCellRenderer(TreeCellRenderer delegate,
			CheckTreeSelectionModel selectionModel) {
		this.delegate = delegate;
		this.selectionModel = selectionModel;
		setLayout(new BorderLayout());
		setOpaque(false);
		checkBox.setOpaque(false);
	}

	@Override
	public Component getTreeCellRendererComponent(JTree tree, Object value,
			boolean selected, boolean expanded, boolean leaf, int row,
			boolean hasFocus) {
		Component renderer = delegate.getTreeCellRendererComponent(tree, value,
				selected, expanded, leaf, row, hasFocus);

		TreePath path = tree.getPathForRow(row);
		if (path != null) {
			if (selectionModel.isPathSelected(path, true))
				checkBox.setSelected(true);
			else if (selectionModel.isPartiallySelected(path))
				checkBox.setIndeterminate();
			else
				checkBox.setSelected(false);
		}

		removeAll();
		add(checkBox, BorderLayout.WEST);
		add(renderer, BorderLayout.CENTER);
		return this;
	}
}

class CheckTreeManager extends MouseAdapter implements TreeSelectionListener {
	private CheckTreeSelectionModel selectionModel;
	private JTree tree = new JTree();
	int hotspot = new JCheckBox().getPreferredSize().width;

	public CheckTreeManager(JTree tree) {
		this.tree = tree;
		selectionModel = new CheckTreeSelectionModel(tree.getModel());
		tree.setCellRenderer(new CheckTreeCellRenderer(tree.getCellRenderer(),
				selectionModel));
		tree.addMouseListener(this);
		selectionModel.addTreeSelectionListener(this);
	}

	@Override
	public void mouseClicked(MouseEvent me) {
		TreePath path = tree.getPathForLocation(me.getX(), me.getY());
		if (path == null)
			return;
		if (me.getX() > tree.getPathBounds(path).x + hotspot)
			return;

		boolean selected = selectionModel.isPathSelected(path, true);
		selectionModel.removeTreeSelectionListener(this);

		try {
			if (selected)
				selectionModel.removeSelectionPath(path);
			else
				selectionModel.addSelectionPath(path);
		} finally {
			selectionModel.addTreeSelectionListener(this);
			tree.treeDidChange();
		}
	}

	public CheckTreeSelectionModel getSelectionModel() {
		return selectionModel;
	}

	@Override
	public void valueChanged(TreeSelectionEvent e) {
		tree.treeDidChange();
	}
}

/*
 * All following classes from
 * http://www.javaspecialists.co.za/archive/Issue145.html
 */
enum TristateState {
	SELECTED {
		@Override
		public TristateState next() {
			return INDETERMINATE;
		}
	},
	INDETERMINATE {
		@Override
		public TristateState next() {
			return DESELECTED;
		}
	},
	DESELECTED {
		@Override
		public TristateState next() {
			return SELECTED;
		}
	};

	public abstract TristateState next();
}

class TristateButtonModel extends ToggleButtonModel {
	private TristateState state = TristateState.DESELECTED;

	public TristateButtonModel(TristateState state) {
		setState(state);
	}

	public TristateButtonModel() {
		this(TristateState.DESELECTED);
	}

	public void setIndeterminate() {
		setState(TristateState.INDETERMINATE);
	}

	public boolean isIndeterminate() {
		return state == TristateState.INDETERMINATE;
	}

	// Overrides of superclass methods
	@Override
	public void setEnabled(boolean enabled) {
		super.setEnabled(enabled);
		// Restore state display
		displayState();
	}

	@Override
	public void setSelected(boolean selected) {
		setState(selected ? TristateState.SELECTED : TristateState.DESELECTED);
	}

	// Empty overrides of superclass methods
	@Override
	public void setArmed(boolean b) {
		// empty
	}

	@Override
	public void setPressed(boolean b) {
		// empty
	}

	void iterateState() {
		setState(state.next());
	}

	private void setState(TristateState state) {
		// Set internal state
		this.state = state;
		displayState();
		if (state == TristateState.INDETERMINATE && isEnabled()) {
			// force the events to fire

			// Send ChangeEvent
			fireStateChanged();

			// Send ItemEvent
			int indeterminate = 3;
			fireItemStateChanged(new ItemEvent(this,
					ItemEvent.ITEM_STATE_CHANGED, this, indeterminate));
		}
	}

	private void displayState() {
		super.setSelected(state != TristateState.DESELECTED);
		super.setArmed(state == TristateState.INDETERMINATE);
		super.setPressed(state == TristateState.INDETERMINATE);

	}

	public TristateState getState() {
		return state;
	}
}

final class TristateCheckBox extends JCheckBox {
	// Listener on model changes to maintain correct focusability
	private final ChangeListener enableListener = new ChangeListener() {
		@Override
		public void stateChanged(ChangeEvent e) {
			TristateCheckBox.this.setFocusable(getModel().isEnabled());
		}
	};

	public TristateCheckBox(String text) {
		this(text, null, TristateState.DESELECTED);
	}

	public TristateCheckBox(String text, Icon icon, TristateState initial) {
		super(text, icon);

		// Set default single model
		setModel(new TristateButtonModel(initial));

		// override action behaviour
		super.addMouseListener(new MouseAdapter() {
			@Override
			public void mousePressed(MouseEvent e) {
				TristateCheckBox.this.iterateState();
			}
		});
		ActionMap actions = new ActionMapUIResource();
		actions.put("pressed", new AbstractAction() {
			@Override
			public void actionPerformed(ActionEvent e) {
				TristateCheckBox.this.iterateState();
			}
		});
		actions.put("released", null);
		SwingUtilities.replaceUIActionMap(this, actions);
	}

	// Next two methods implement new API by delegation to model
	public void setIndeterminate() {
		getTristateModel().setIndeterminate();
	}

	public boolean isIndeterminate() {
		return getTristateModel().isIndeterminate();
	}

	public TristateState getState() {
		return getTristateModel().getState();
	}

	// Overrides superclass method
	@Override
	public void setModel(ButtonModel newModel) {
		super.setModel(newModel);

		// Listen for enable changes
		if (model instanceof TristateButtonModel)
			model.addChangeListener(enableListener);
	}

	// Empty override of superclass method
	@Override
	public synchronized void addMouseListener(MouseListener l) {
		// empty
	}

	// Mostly delegates to model
	private void iterateState() {
		// Maybe do nothing at all?
		if (!getModel().isEnabled())
			return;

		grabFocus();

		// Iterate state
		getTristateModel().iterateState();

		// Fire ActionEvent
		int modifiers = 0;
		AWTEvent currentEvent = EventQueue.getCurrentEvent();
		if (currentEvent instanceof InputEvent) {
			modifiers = ((InputEvent) currentEvent).getModifiers();
		} else if (currentEvent instanceof ActionEvent) {
			modifiers = ((ActionEvent) currentEvent).getModifiers();
		}
		fireActionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
				getText(), System.currentTimeMillis(), modifiers));
	}

	// Convenience cast
	public TristateButtonModel getTristateModel() {
		return (TristateButtonModel) super.getModel();
	}
}
